package com.googlecode.dgwt.rebind;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.googlecode.dgwt.client.dojo.Dojo;
import com.googlecode.dgwt.client.dojo.core.JsFunction;
import com.googlecode.dgwt.client.dojo.impl.DojoImpl;
import com.googlecode.dgwt.client.dojo.reflect.BeanField;
import com.googlecode.dgwt.client.dojo.reflect.BeanMethod;
import com.googlecode.dgwt.client.dojo.reflect.BeanParam;
import com.googlecode.dgwt.client.dojo.reflect.DojoBean;
import com.googlecode.dgwt.rebind.base.BaseSourceCreator;

/**
 * @author      Wellington Tadeu dos Santos
 * @since       DGWT v0.01
 */
public class ReflectionSourceCreator extends BaseSourceCreator {

    private JClassType targetClass;
    private JClassType beanClass;
    private SourceWriter writer;
    
    private StringBuffer buffer = new StringBuffer(40);
    private String currentBeanPrefix;
    
    private Set<JClassType> allClasses = new HashSet<JClassType>();
    private Map<String,JMethod> allMethods = new HashMap<String,JMethod>();
    private Map<String,FieldEntry> allBeanMethods = new HashMap<String,FieldEntry>();
    
    
    public ReflectionSourceCreator(GeneratorContext context, TreeLogger logger, TypeOracle typeOracle) {
        super(context, logger, typeOracle);
        beanClass = typeOracle.findType(DojoBean.class.getName());
    }

    
    @Override
    public String emitClass(JClassType aTargetClass) throws UnableToCompleteException {
        this.targetClass = aTargetClass;
        
        String packageName = targetClass.getPackage().getName();
        String newClassName = targetClass.getName() + "_DGWT";
        PrintWriter pw = context.tryCreate(logger, packageName, newClassName);

        if (pw != null) {
            ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(packageName, newClassName);
            factory.setSuperclass(targetClass.getQualifiedSourceName());
            
            factory.addImport(BeanMethod.class.getName());
            factory.addImport(BeanField.class.getName());
            factory.addImport(JsFunction.class.getName());
            factory.addImport(BeanParam.class.getName());
            
            writer = prepareSourceWriter(factory, factory.createSourceWriter(context, pw), packageName, newClassName);
            writer.println();
            writer.print("public ");
            writer.print(newClassName);
            writer.println("(){");
            writer.indent();
            {
                JClassType[] types = beanClass.getSubtypes();                
                generateAllMethods(typeOracle.getJavaLangObject());
                
                for (JClassType type : types) {
                    generateAllMethods(type);
                }
                
                writer.println();

                for (JClassType type : allClasses) {
                    writer.print("infoMap.put(");
                    writer.print(type.getQualifiedSourceName());
                    writer.print(".class.getName(),");
                    writer.print(getClassVar(type));
                    writer.println(");");
                }
            }
            writer.outdent();
            writer.println("}");
            writer.println();

            writer.println("protected native <T extends Cloneable> T clone(T obj) /*-{");
            writer.indent();
            {
                if (isJavaMode()) {
                    writer.println("return obj.@java.lang.Object::clone()()");
                } else {
                    writer.println("var d=@" + Dojo.class.getName() + "::impl;");
                    writer.println("return d.@" + DojoImpl.class.getName() + "::nativeClone(Ljava/lang/Cloneable;)(obj);");
                }
            }
            writer.outdent();
            writer.println("}-*/;");
            writer.println();
            writer.commit(logger);
        }
        
        return packageName + "." + newClassName;       
    }

    private void generateAllMethods(JClassType type) {
        currentBeanPrefix = type.getQualifiedSourceName() + ":";
        allBeanMethods.clear();
        generateAllMethodsAux(type);
    }
    
    private void generateAllMethodsAux(JClassType type) {
        
        if (type == null || !allClasses.add(type)){
            return;
        }
        
        if (type != typeOracle.getJavaLangObject()) {
            generateAllMethods(type.getSuperclass());
        }
        
        writer.println();
        writer.println();
        writer.println("////////////////////////////////////////////////////");
        writer.print("//  ");
        writer.println(type.getQualifiedSourceName());
        writer.println("////////////////////////////////////////////////////");
        writer.println();
        writer.println();
        
        for (JMethod method : type.getMethods()) {
            
            if (!method.isPublic() || method.isStatic()) {
                continue;
            }
            
            String sign = uniqueMethodKey(method);
            
            if (allMethods.containsKey(sign)) {
                continue;
            }
            
            allMethods.put(sign, method);
            
            writer.print("BeanMethodImpl ");
            writer.print(getMethodVar(method, false));
            writer.print(" = new BeanMethodImpl(\"");
            writer.print(method.getName());
            writer.print("\", ");
            writer.print(getReturnTypeClassString(method));
            writer.print(", new " + BeanParam.class.getSimpleName() + "[");
            
            JParameter[] params = method.getParameters();

            if (params.length > 0) {
                writer.print("]{");
                boolean first = true;
                
                for (JParameter param : params) {
                    if (first) {
                        first = false;
                    } else {
                        writer.print(", ");
                    }
                    writer.print("new ");
                    writer.print(BeanParam.class.getSimpleName());
                    writer.print("(");
                    writer.print(wrap(param.getName()));
                    writer.print(",");                    
                    generateParamType(param.getType());
                    writer.print(".class, \"");
                    writer.print(param.getType().getJNISignature());
                    writer.print("\")");
                }
                writer.print("}");
            } else {
                writer.print("0]");
            }
            
            writer.println("){");
            writer.indent();
            {
                writer.println("public native " + JsFunction.class.getSimpleName() + " getFunction() /*-{");
                writer.indent();
                {
                    writer.println("return function(){");
                    writer.indent();
                    {
                        if (params.length > 0) {
                            writer.println("var a=arguments;");
                        }
                        
                        writer.print("return this.@");
                        writer.print(type.getQualifiedSourceName());
                        writer.print("::");
                        writer.print(method.getName());
                        writer.print("(");
                        
                        for (JParameter param : params) {
                            writer.print(param.getType().getJNISignature());
                        }
                        
                        writer.print(")(");
                        
                        for (int i=0; i<params.length; i++) {
                            if (i != 0) {
                                writer.print(", ");
                            }
                            writer.print("a[" + i + "]");
                        }
                        
                        writer.println(");");
                    }
                    writer.outdent();
                    writer.println("};");
                }
                writer.outdent();
                writer.println("}-*/;");
            }
            writer.outdent();
            writer.println("};");
            writer.println();
        }
    
        writer.print("BeanInfoImpl ");
        writer.print(getClassVar(type));
        writer.print(" = BeanInfoImpl.create(");
        writer.print(type.getQualifiedSourceName());
        writer.print(".class, new " + BeanMethod.class.getSimpleName() + "[");
        
        if (!allMethods.isEmpty()) {
            writer.println("]{");
            writer.indent();
            writer.indent();
            {
                boolean first = true;
    
                for (JMethod method : allMethods.values()) {
                    if (first) {
                        first = false;
                    } else {
                        writer.print(", ");
                    }
                    writer.print(getMethodVar(method));
                }
            }
            writer.println();
            writer.outdent();
            writer.outdent();
            writer.print("}");
        } else {
            writer.print("0]");
        }
        
        writer.print(", new " + BeanField.class.getSimpleName() + "[");
        findBeanMethods(type);
        
        if (!allBeanMethods.isEmpty()) {
            {
                writer.println("]{");
                writer.indent();
                boolean first = true;
                
                for (FieldEntry info : allBeanMethods.values()) {
                    if (first) {
                        first = false;
                    } else {
                        writer.println(", ");
                    }
                    writer.print(info.toString(this));
                }
                
                writer.println();
                writer.outdent();
                writer.print("}");
            }
        } else {
            writer.print("0]");            
        }
        writer.println(");");
        writer.println();
    }
    
    
    private void generateParamType(JType paramType) {
        JPrimitiveType primitiveType = paramType.isPrimitive();
        
        if (primitiveType != null) {
            writer.print(getJavaObjectTypeFor(primitiveType));
            return;
        }
        
        JArrayType arrayType = paramType.isArray();
        
        if (arrayType != null) {
            generateParamType(arrayType.getComponentType());
            writer.print("[]");
            return;
        }
        
        JClassType classType = paramType.isClassOrInterface();
        
        if (classType == null) {
            classType = paramType.isEnum();
        }
        
        if (classType != null) {
            writer.print(classType.getQualifiedSourceName());
            return;
        }        
        
        writer.print(typeOracle.getJavaLangObject().getName());
        logger.log(Type.WARN, "Param type not recognized: \"" + paramType.getQualifiedSourceName() + "\"");
    }


    private void findBeanMethods(JClassType type) {
        JMethod[] methods = type.getMethods();

        final int GETTER = 1;
        final int SETTER = 2;
        
        for (JMethod method : methods){
            
            if (!method.isPublic() || method.isStatic()) {
                continue;
            }
            
            String methodName = method.getName();
            int methodType = 0;
            buffer.setLength(0);
            

            if (method.getParameters().length == 0
                    && !("void".equals(method.getReturnType().getQualifiedSourceName()))){
                
                if (methodName.startsWith("get")) {
    
                    methodType = GETTER;
                    buffer.append(methodName);
                    buffer.delete(0, 3);
                    
                } else if (methodName.startsWith("is")){
                
                    methodType = GETTER;
                    buffer.append(methodName);
                    buffer.delete(0, 2);
                }
                
            } else if (method.getParameters().length == 1 
                    && methodName.startsWith("set")){
                
                methodType = SETTER;
                buffer.append(methodName);
                buffer.delete(0, 3);                
            }

            if (methodType == 0) {
                continue;
            }
            
            if (buffer.length() > 0) {
                buffer.setCharAt(0, Character.toLowerCase(buffer.charAt(0)));
            }

            String fieldName = buffer.toString();
            FieldEntry entry = allBeanMethods.get(fieldName);
            
            if (entry == null) {
                entry = new FieldEntry(fieldName);
                allBeanMethods.put(fieldName, entry);
            }
            
            if (methodType == GETTER) {
                entry.getter = method;
            } else {
                JType paramType;
                if (entry.setter != null 
                        && (paramType = entry.setter.getParameters()[0].getType()) == entry.getter.getReturnType()
                        && paramType != method.getParameters()[0].getType()) {
                    // FIXME remove log
                    logger.log(Type.WARN, "Method overload: " + uniqueMethodKey(method));
                    continue;
                }
                if (entry.setter != null) {
                    // FIXME remove log
                    logger.log(Type.WARN, "Method overload: " + uniqueMethodKey(entry.setter));
                }
                entry.setter = method;
            }
        }        
    }

    
    protected String uniqueMethodKey(JMethod method) {
        String name = currentBeanPrefix + method.getName();
        name += "(";
        JParameter[] m = method.getParameters();
        for (int i = 0; i < m.length; i++) {
            name += m[i].getType() + " ";
        }
        name += ")";
        return name;
    }


    private String getMethodVar(JMethod method) {
        return getMethodVar(method, true);
    }
    
    
    private String getMethodVar(JMethod method, boolean lookup) {
        if (lookup) {
            method = allMethods.get(uniqueMethodKey(method));
        }
        return "m" + System.identityHashCode(method);
    }
    

    private String getClassVar(JClassType classType) {
        return "c" + System.identityHashCode(classType);
    }

    
    private String getReturnTypeClassString(JMethod method) {
        JType type = method.getReturnType();
        return "void".equals(type.getQualifiedSourceName()) ? "null" : type.getQualifiedSourceName() + ".class";
    }

    
    static class FieldEntry {        
        
        String fieldName;
        JMethod getter;
        JMethod setter;
        
        public FieldEntry(String fieldName) {
            this.fieldName = fieldName;
        }

        public String toString(ReflectionSourceCreator source) {
            source.buffer.setLength(0);
            source.buffer.append("new " + BeanField.class.getSimpleName() + "<Object>(\"");
            source.buffer.append(fieldName);
            source.buffer.append("\", ");
            source.buffer.append(getter == null ? "null" : source.getMethodVar(getter));
            source.buffer.append(", ");
            source.buffer.append(setter == null ? "null" : source.getMethodVar(setter));
            source.buffer.append(")");
            return source.buffer.toString();
        }
    }

    //FIXME private Boolean javaMode;
    
    private boolean isJavaMode() {
        /*if (javaMode == null) {
            javaMode = true;
            try {
                logger.log(Type.WARN, "OK-"+ JavaScriptObject.createObject());
            } catch (Throwable e) {
                logger.log(Type.WARN, "FAIL-" + e.getMessage());
                javaMode = false;
            }
        }*/
        /*JClassType jsoType = typeOracle.findType(JsValueGlue.JSO_CLASS);
        logger.log(Type.WARN, "jsoType=="+jsoType);
        */
        
        /*
        try {
            Class<?> gwtClass = Class.forName("com.google.gwt.core.client.GWT");
            return (Boolean) gwtClass.getDeclaredMethod("isClient").invoke(gwtClass);
        } catch (Exception e) {
            logger.log(Type.ERROR, e.getMessage(), e);
            return true;
        }
        */
        
        // typeOracle.fType(name) 
        
        /*try {
            logger.log(Type.WARN,"dojo.javaMode=="+ context.getPropertyOracle().getPropertyValue(logger, "dojo.javaMode"));
            return "true".equals(context.getPropertyOracle().getPropertyValue(logger, "dojo.javaMode"));
        } catch (BadPropertyValueException e) {
            logger.log(Type.WARN, "Could not parse property \"dojo.javaMode\"");
            return true;
        }*/
 
        return false;
    }

}
